package xyz.tanxiao.links.core.limit;

import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import xyz.tanxiao.links.common.enums.LimitTargetEnum;
import xyz.tanxiao.links.common.exception.AccessLimitException;
import xyz.tanxiao.links.common.utils.NetworkUtils;
import xyz.tanxiao.links.config.GlobalProperties;
import xyz.tanxiao.links.entity.SlIpBlacklist;
import xyz.tanxiao.links.service.SlIpBlacklistService;

import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
 * 计数器访问注解切面类
 *
 * @author 万成波
 * @email 2300064869@qq.com
 * @createTime 2023-06-16 16:34:08 星期五
 */
@RequiredArgsConstructor
@Aspect
@Component
public class SimpleAccessLimitAspect {

	private final RedisTemplate<String, Object> redisTemplate;

	private final DefaultRedisScript<Long> defaultRedisScript;

	private final ApplicationEventPublisher eventPublisher;

	private final GlobalProperties globalProperties;

	private final SlIpBlacklistService blacklistService;


	@Pointcut("@annotation(xyz.tanxiao.links.core.limit.SimpleAccessLimits)")
	public void pointcut() {}


	/**
	 * 环绕通知
	 */
	@Around("pointcut()")
	public Object around(ProceedingJoinPoint point) throws Throwable {
		// 未启用接口限流
		if (!this.globalProperties.isEnableLimit()) {
			return point.proceed();
		}

		// 限流接口
		MethodSignature signature = (MethodSignature) point.getSignature();
		SimpleAccessLimits limits = signature.getMethod().getAnnotation(SimpleAccessLimits.class);

		String ip = NetworkUtils.getRequestIp();
		if (!StringUtils.hasText(ip)) {
			throw new AccessLimitException("请求失败，当前访问受限，请稍后再试。");
		}

		// 限流计数检查
		this.checkRequestIp(limits, ip);
		this.checkBeforeLimits(limits, ip);

		Object proceed = point.proceed();

		// 添加限流计数
		this.checkAfterLimits(limits, ip);
		return proceed;
	}



	/**
	 * 请求ip检查
	 *
	 * @param limits 限流策略
	 * @param ip 请求ip
	 */
	private void checkRequestIp(SimpleAccessLimits limits, String ip) {
		SimpleAccessLimit limit = limits.value()[0];
		if (Objects.isNull(limit)) {
			return;
		}
		List<SlIpBlacklist> blacklists = this.blacklistService.getBlacklist(this.globalProperties.getTriggerCount());
		blacklists.forEach(blacklist -> {
			if (blacklist.getClientIp().equals(ip)) {
				throw limit.target().getLimitException("请求失败，当前请求已被禁止无法访问。");
			}
		});
	}


	/**
	 * 请求ip限流检查 (前)
	 *
	 * @param limits 限流策略
	 * @param ip 请求ip
	 */
	private void checkBeforeLimits(SimpleAccessLimits limits, String ip) {
		for (SimpleAccessLimit limit : limits.value()) {
			// 限流过期时间、限流缓存key
			long expiredTime = limit.timeUnit().toSeconds(limit.time());
			LimitTargetEnum target = limit.target();
			String limitCacheKey = target.getLimitCacheKey(limit.level(), ip);

			// 执行限流Lua脚本，查询接口限流计数。
			Long countTotal = this.redisTemplate.execute(
				this.defaultRedisScript, Collections.singletonList(limitCacheKey), expiredTime
			);

			// 接口限流检查
			if (Objects.isNull(countTotal) || countTotal >= limit.count()) {
				this.eventPublisher.publishEvent(new SlIpBlacklist(ip));
				throw target.getLimitException();
			}
		}
	}


	/**
	 * 请求ip限流检查 (后)
	 *
	 * @param limits 限流策略
	 * @param ip 请求ip
	 */
	private void checkAfterLimits(SimpleAccessLimits limits, String ip) {
		for (SimpleAccessLimit limit : limits.value()) {
			String cacheKey = limit.target().getLimitCacheKey(limit.level(), ip);
			this.redisTemplate.opsForValue().increment(cacheKey);
		}
	}

}
